In [1]:
from IPython.display import HTML

HTML('''<script>
code_show=true; 
function code_toggle() {
 if (code_show){
 $('div.input').hide();
 } else {
 $('div.input').show();
 }
 code_show = !code_show
} 
$( document ).ready(code_toggle);
</script>
<form action="javascript:code_toggle()"><input type="submit" value="Click here to toggle on/off the raw code."></form>''')
Out[1]:

Analysis of OBD2 data logged from bluetooth adapter

Car: Mercedes-Benz C-Class 250 T Wagon S204 phase-II 7G-Tronic (2011)

Data from: https://www.automobile-catalog.com/car/2011/1551800/mercedes-benz_c_250_blueefficiency_t-modell_7g-tronic.html

In [2]:
import pandas as pd
import numpy as np
from numpy import inf

import plotly.offline as py
import plotly.graph_objs as go
import plotly.tools as tools

# Set notebook mode to work in offline
py.init_notebook_mode()

Car data

In [3]:
measured_fuel_consumption = [15.06, 16.09, 15.54, 15.29, 15.09, 14.11, 14.95, 14.89, 15.55, 15.74, 16.98, 15.57]

torque_engine_max = 500; # Nm

gears = [4.377, 2.859, 1.921, 1.368, 1, 0.82, 0.728]
#gears_overall = [10.81, 7.06, 4.74, 3.38, 2.47, 2.03, 1.8]
final_ratio = 2.47
reverse = 3.416

wheel = {'width_mm':225, 'profile_perc':45, 'diameterRim_inches':17}
wheel['diameterRim_mm'] = wheel['diameterRim_inches']*25.4
wheel['diameterTot_m'] = (wheel['diameterRim_mm'] + 2*(wheel['profile_perc']/100*wheel['width_mm']) )/1000

print("Max engine torque", torque_engine_max, " Nm")
print("Wheel data", wheel)
print("Gear ratio: ", gears)
print("Final ratio: ", final_ratio)
print("Fuel consumption estimated from tank refills:", measured_fuel_consumption)
Max engine torque 500  Nm
Wheel data {'width_mm': 225, 'profile_perc': 45, 'diameterRim_inches': 17, 'diameterRim_mm': 431.79999999999995, 'diameterTot_m': 0.6343}
Gear ratio:  [4.377, 2.859, 1.921, 1.368, 1, 0.82, 0.728]
Final ratio:  2.47
Fuel consumption estimated from tank refills: [15.06, 16.09, 15.54, 15.29, 15.09, 14.11, 14.95, 14.89, 15.55, 15.74, 16.98, 15.57]
In [4]:
df = pd.read_csv('logs/2019-04-12h06_29_02.761616_obdData.log',sep=';')
df['Time_s'] = df['Time_s']-df['Time_s'][0]

df['ENGINE_LOAD_Nm']=df['ENGINE_LOAD'].apply(lambda x: x*torque_engine_max/100)

df['ENGINE_POWER_W']=df['ENGINE_LOAD_Nm']*(df['RPM'].apply(lambda x: x*2*np.pi/60))
df['ENGINE_POWER_hp']=df['ENGINE_POWER_W'].apply(lambda x: x*0.00134102209)


df['SPEED'] = df['SPEED'] + 0 #+3 correct for actual speed (TO BE CHEDKED WITH GPS=
In [5]:
#df

Engine map

Engine load data is clipped to 100%, and not really very usefull to assess behavior at high torques. For now let's assume that the engine torque is the engine load data % multiplied the maximum torque the engine can deliver.

TODO: BSFC once the instantaneous fuel flow is estimated. SEE AT THE BOTTOM, section BSFC.

In [6]:
fig = tools.make_subplots(rows=1, cols=2)

pRpmPower = go.Scatter(
    x=df['RPM'],#df['Time_s'],
    y=df['ENGINE_LOAD_Nm'],
    mode='markers'
)
fig.append_trace(pRpmPower, 1, 1)    
pRpmTorque = go.Scatter(
    x=df['RPM'],#df['Time_s'],
    y=df['ENGINE_POWER_hp'],
    mode='markers'
)
fig.append_trace(pRpmTorque, 1, 2)

fig['layout']['xaxis1'].update(title='Engine (RPM)')
fig['layout']['xaxis2'].update(title='Engine load (Nm)')
fig['layout']['yaxis1'].update(title='Engine (RPM)')
fig['layout']['yaxis2'].update(title='Engine power (HP)')
fig['layout'].update(height=500, width=1000)

py.iplot(fig)
This is the format of your plot grid:
[ (1,1) x1,y1 ]  [ (1,2) x2,y2 ]

Note that the estimated engine power for some outliers is above the nominal engine power of 200 horse power.

Analyze gear

In [7]:
pGear = [go.Scatter(
    x=df['RPM'],
    y=df['SPEED'],
    mode='markers',
    name = 'Data')]

rpm = np.linspace(0, max(df['RPM'])+100, num=2)

gears_overall = []

for i in range(0,len(gears)):
    wheel_rpm   = rpm/(gears[i]*final_ratio)
    wheel_radps = wheel_rpm*2*np.pi/60
    kmph        = wheel_radps*(wheel['diameterTot_m']/2)*3.6
    gears_overall.append(kmph[1]/rpm[1])
    pGear.append(
        go.Scatter(
            x=rpm,
            y=kmph,
            name = str(i+1)+' gear'
        )
    )
In [8]:
RPM_idle = 650

dist_thrs = 5 # km/h

indxs = [[], [], [], [], [], [], []]
for i in range(0, len(df['SPEED'])):
    x = df['RPM'][i]
    y = df['SPEED'][i]
    
    distances = []
    indx_min = 0
    dist_min = 1e6
    if x <= RPM_idle:
        indx_min = 0
    else:
            
        for j in range(0, len(gears_overall)):
            d = (abs(gears_overall[j]*x - 1*y )/np.sqrt(gears_overall[j]**2+1))
            if d < dist_min:
                indx_min = j
                dist_min = d
        if dist_min < dist_thrs:
            indxs[indx_min].append(i)   
In [9]:
df_1 = df.loc[indxs[0], :]
df_2 = df.loc[indxs[1], :]
df_3 = df.loc[indxs[2], :]
df_4 = df.loc[indxs[3], :]
df_5 = df.loc[indxs[4], :]
df_6 = df.loc[indxs[5], :]
df_7 = df.loc[indxs[6], :]

columns = ['Time in gear (%)', 'Average torque in gear (%)']
df_gearStatistics = pd.DataFrame(columns=columns)

print('Statistics')
for i in range(0, len(indxs)):
    percTimeInGear = np.round(len(df.loc[indxs[i], :])/len(df)*100)
    avrgTorqueInGear = np.round( np.mean(df['ENGINE_LOAD'][indxs[i]]))
    #print('\tgear ', i+1, ': percTimeInGear', percTimeInGear, '%, avg.Trq ',  round(avrgTorqueInGear), '%')
    
    df_gearStatistics.loc[i] = [percTimeInGear, avrgTorqueInGear]
Statistics

Plot on the left: logged engine speed and vehicle speed it is possible to assess the gear ratios of the 7-gear automatic transmission. The match between measured ratios and nominal ones from datasheet is good, meaning that the error of the measured speed is small.

Plot on the right: by plotting the vehicle speed vs the engine load and colouring the points based on estimated gear it should be possible to reverse engineer the gear shifting strategy. However, the data is very noisy, mainly due to the fact that this automatic transmission has a torque converter, hence a complex behavior.

In [10]:
colors = ['rgba(200, 0, 0, 0.2)',
        'rgba(150, 50, 0, 0.2)',
        'rgba(100, 100, 0, 0.2)',
        'rgba(50, 150, 100, 0.2)',
        'rgba(0, 200, 0, 0.2)',
        'rgba(0, 150, 50, 0.2)',
        'rgba(0, 100, 100, 0.2)',]

colors = ['rgba(200, 0, 0, 0.2)',
        'rgba(0, 200, 0, 0.2)',
        'rgba(0, 0, 200, 0.2)',
        'rgba(150, 50, 0, 0.2)',
        'rgba(0, 50, 100, 0.2)',
        'rgba(100, 0, 100, 0.2)',
        'rgba(75, 75, 75, 0.2)',]
pGearColors   = []
pEngineColors = []
fig = tools.make_subplots(rows=1, cols=2)

for i in range(0,len(gears)):
    pGearColors = (
        go.Scatter(
            x=rpm,
            y=gears_overall[i]*rpm,
            name = str(i+1)+' gear',
            mode='lines',
            line = dict(color = colors[i]),
            showlegend=False
        )
    )
    fig.append_trace(pGearColors, 1, 1)
    pGearColors = (
        go.Scatter(
            x= df['RPM'][indxs[i]],
            y= df['SPEED'][indxs[i]],
            #name = str(i+1)+' gear',
            mode='markers',
            marker = dict(color = colors[i]),
            showlegend=False
        )
    )
    fig.append_trace(pGearColors, 1, 1)
    
    pEngineColors = (
        go.Scatter(
            x= df['SPEED'][indxs[i]],
            y=df['ENGINE_LOAD_Nm'],
            #name = str(i+1)+' gear',
            mode='markers',
            marker = dict(color = colors[i]),
            showlegend=False
        )
    )
    fig.append_trace(pEngineColors, 1, 2)

#fig.append_trace(pGearColors, 1, 1)
#fig.append_trace(pEngineColors, 1, 2)

fig['layout']['xaxis1'].update(title='Engine (RPM)')
fig['layout']['xaxis2'].update(title='Vehicle speed (km/h)')
fig['layout']['yaxis1'].update(title='Vehicle speed (km/h)')
fig['layout']['yaxis2'].update(title='Engine load (Nm)')
fig['layout'].update(height=500, width=1000)

py.iplot(fig)
# Classifier to reverse engineer the gear shifting strategy
This is the format of your plot grid:
[ (1,1) x1,y1 ]  [ (1,2) x2,y2 ]

In [11]:
df_gearStatistics
Out[11]:
Time in gear (%) Average torque in gear (%)
0 3.0 37.0
1 7.0 18.0
2 8.0 20.0
3 7.0 17.0
4 11.0 19.0
5 10.0 25.0
6 31.0 24.0
In [12]:
trace0 = go.Scatter(
    x=df_gearStatistics.index,
    y=df_gearStatistics['Time in gear (%)'],
    mode='markers',
    marker=dict(
        color=df_gearStatistics['Average torque in gear (%)'],
        size=df_gearStatistics['Average torque in gear (%)'],
        showscale=True
    )
)
data = [trace0]

layout = go.Layout(
    title='Gear management statistics',
    xaxis=dict( title='Gear (/)' ),
    yaxis=dict( title='Time in gear (%)' )
)

py.iplot(go.Figure(data=data, layout=layout))

Estimate fuel consumption

Fuel consumption is estimated by assuming: A2D = 14.6 air-to-diesel stechiometric ratio DD = density of diesel = 0.832 # kg/litre

Measured values:
MAF: (grams/sec) Mass Air Flow
S : (km/h) vehicle longitudinal speed

The fuel mass flow MFF (grams/sec) is:
MFF = MAF/A2D

Fuel volumetric flow (litre/h):
VFF = MFF/(DD*1000)*3600

Fuel consumption KMPL (km/litre):
KMPL = S/VFF

TODO: fuel consumption seems to be over estimated. Correction factor needed, because fuel tank refill statistics state that the average fuel consumption is 15 km/l.

In [13]:
df['MAF'] = df['MAF']
pMAF = go.Scatter(
    x=df['Time_s'],
    y=df['MAF'],
    name = 'MAF'
    #mode='markers+text'
)

pSPEED = go.Scatter(
    x=df['Time_s'],
    y=df['SPEED'],
    name = 'SPEED'
    #mode='markers+text'
)

layout = go.Layout(
    #title='Powertrain performance',
    xaxis=dict( title='Time (s)' ),
    yaxis=dict( title='' )
)

#fig = go.Figure(data=pGear, layout=layout)
#py.iplot([pSPEED, pMAF])

layout = go.Layout(
    title='Powertrain performance',
    xaxis=dict( title='Time (s)' ),
    yaxis=dict( title='' )
)

py.iplot({"data":[pSPEED, pMAF], "layout":layout})
In [14]:
# Natural gas: 17.2
# Gasoline: 14.7
# Propane: 15.5
# Ethanol: 9
# Methanol: 6.4
# Hydrogen: 34
# Diesel: 14.6

AIR2DIESEL_RATIO = 14.6
DENSITYDIESEL    = 0.832 # kg/litre !!! ADJUST THIS VALUE IF NEEDED

df['MFF']  = df['MAF']/AIR2DIESEL_RATIO # (grams/sec) Mass Fuel Flow
df['VFF']  = df['MFF']/(DENSITYDIESEL*1000)*3600 # (litres/h) Volume Fuel Flow


df['KMPL'] = df['SPEED']/df['VFF'] # (km/litre) Fuel consumption
In [15]:
pMFF = go.Scatter(
    x=df['Time_s'],
    y=df['MFF'],
    name = 'MFF'
    #mode='markers+text'
)
pKMPL = go.Scatter(
    x=df['Time_s'],
    y=df['KMPL'],
    name = 'KMPL'
    #mode='markers+text'
)
pVFF = go.Scatter(
    x=df['Time_s'],
    y=df['VFF'],
    name = 'VFF'
    #mode='markers+text'
)

layout = go.Layout(
    title='Economy',
    xaxis=dict( title='Time (s)' ),
    yaxis=dict( title='Fuel consumption (km/l)' )
)

py.iplot({"data":[pKMPL], "layout":layout})

It is possible to compare the estimated fuel consumption against the fuel consumption measured by filling the tank at every refuel.

In [16]:
print("Estimated average fuel consumption while moving: ", np.mean(df['KMPL']), " km/l")
print("Measured average fuel consumption: ", np.mean(measured_fuel_consumption), " km/l")
Estimated average fuel consumption while moving:  7.754293359537499  km/l
Measured average fuel consumption:  15.405  km/l

The estimated fuel consumption is 50% off the measured value, hance a factor of approx. 2 is needed to correct the estimation.

Brake Specific Fuel Consumption

Assuming a correction factor of 2 for the estimated fuel consumption KMPL such that its average value is closer to the measured fuel consumption:
KMPL_corr = 2*KMPL

and that the engine load in Nm is it is possible to try to reconstruct the engine BSFC map.

In [17]:
correction_factor = 2
df['BSFC'] = correction_factor*df['MFF']/df['ENGINE_POWER_W'];
df['BSFC'] = df['BSFC'].apply(lambda x: x*3.6*1e6)
In [18]:
df_red = df[(df['ENGINE_LOAD_Nm']>50) & (df['RPM']>500)]

trace0 = go.Scatter(
    x=df_red['RPM'],
    y=df_red['ENGINE_LOAD_Nm'],
    mode='markers',
    marker=dict(
        color=df_red['BSFC'],
        size=10,
        showscale=True, 
        opacity = 0.5,
    )
)
data = [trace0]

layout = go.Layout(
    title='BSFC g/(kWâ‹…h)',
    xaxis=dict( title='Engine speed (RPM)' ),
    yaxis=dict( title='Engine load (Nm)' ),
    width=900,
    height=700
)

py.iplot(go.Figure(data=data, layout=layout))
In [ ]:
 
In [ ]: